﻿/*
 * Copyright 2011 Edward L. Stauff.                  Contact: <EdStauff@gmail.com>.
 * 
 * This file, "ConsoleApp.cs", is part of the "Stauffware.Common" class library.
 * "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * the "Stauffware.Common" sources.  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using Stauffware.Common.Core;
using System.Threading;
using CMD=Stauffware.Common.Console;

namespace Stauffware.Common.Console
{
	/// <summary>
	/// Provides a framework for a console application, including error reporting,
	/// exception handling, and processing of command-line parameters.  To use it: 
	/// 1. Create a subclass of ConsoleApp and implement Run().
	/// 2. In your Main(), instantiate your subclass of ConsoleApp and call its
	///		Start() method.
	/// </summary>
	///<example>
	///<![CDATA[
	///	
	///		namespace MyLittleTestApp
	///		{
	///			class MyLittleConsoleApp : ConsoleApp
	///			{
	///				static int Main (string[] args)
	///				{
	///					return (new MyLittleConsoleApp()).Start(args);
	///				}
	///				
	///				protected override AppExitCode Run()
	///				{
	///					// ... application code ...
	///				}
	///			}
	///		}
	/// 
	///]]>
	///</example>

	public abstract class ConsoleApp
	{
		/// <summary>
		/// Gets the one-and-only instance of ConsoleApp.
		/// </summary>
		public static ConsoleApp Current { get { return _current; }	}
		private static ConsoleApp _current;

		/// <summary>
		/// Gets or sets a string that will precede any message reported by FatalError().
		/// </summary>
		public string FatalErrorPrefix
		{
			get { return this._fatalErrorPrefix; }
			set
			{
				Require.NotNull(value, "Do not set ConsoleApp.FatalErrorPrefix to null.");
				this._fatalErrorPrefix = value;
			}
		}
		private string _fatalErrorPrefix = "*** FATAL ERROR: ";

	//	private object CmdLineParamsTarget = null;
		private List<object> CmdLineParamsTargets = new List<object>();

		private Thread StartThread;		// the thread in which Start was called

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Standard application exit codes.
		/// </summary>

		public static class ExitCodes
		{
			/// <summary>
			/// Indicates complete and unqualified success.
			/// </summary>
			public static readonly AppExitCode Success = 
				new AppExitCode(0, false, "success");

			/// <summary>
			/// Indicates that the program displayed help and did nothing else.
			/// </summary>
			public static readonly AppExitCode HelpOnly = 
				new AppExitCode(-1, false, "help displayed");

			/// <summary>
			/// Indicates that the program terminated because of an unhandled exception.
			/// </summary>
			public static readonly AppExitCode UnhandledException = 
				new AppExitCode(1010, true, "unhandled exception");

			/// <summary>
			/// Indicates errors in the program's command-line parameters.
			/// </summary>
			public static readonly AppExitCode CmdLineParams = 
				new AppExitCode(1000, true, "error in command-line arguments");
			
			/// <summary>
			/// A catch-all error code for those who are too <strike>lazy</strike> 
			/// busy to define specific ones.
			/// </summary>
			public static readonly AppExitCode UnspecifiedError = 
				new AppExitCode(1020, true, "unspecified error (programmer was too lazy to define one)");
		}

		//-----------------------------------------------------------------------------

		#region Constructors and Start methods

		//-----------------------------------------------------------------------------
		/// <summary>
		/// The constructor.
		/// </summary>

		protected ConsoleApp ( )
		{
			Require.Condition(_current == null, "You can only instantiate one ConsoleApp object.");
			_current = this;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// <para>Starts execution of the console application.  
		/// You should call this method in your "main" method as follows:</para>
		/// <code>return (new <i>MyConsoleAppSubclass</i>()).Start(cmdLineParams);</code>
		/// </summary>
		/// <param name="cmdLineParams">
		///		the command-line parameters array that was passed to your "main" method
		/// </param>
		/// <returns>the application exit code to be returned by your "main" method</returns>
		/// <remarks>
		/// This overload of Start() will catch and gracefully report unhandled exceptions.
		/// If your application takes command-line parameters, you will need to override
		/// the ParseCmdLineParams method.  Other overloads of Start() provide other
		/// capabilities.
		/// </remarks>

		protected int Start (string[] cmdLineParams)
		{
			return this.Start(cmdLineParams, null, true);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// <para>Starts execution of the console application.  
		/// You should call this method in your "main" method as follows:</para>
		/// <code>return (new <i>MyConsoleAppSubclass</i>()).Start(<i>myCmdLineParams</i>, 
		///			<i>myParamsObject</i>, !System.Diagnostics.Debugger.IsAttached);</code>
		/// </summary>
		/// <param name="cmdLineParams">
		///		the command-line parameters array that was passed to your "main" method
		/// </param>
		/// <param name="paramsTarget">
		///		The object to receive the command-line parameter values.  
		///		If this is not null, then ParseCmdLineParams() will, using CmdLineParser, 
		///		parse the command line params into the paramsTarget.
		/// </param>
		/// <param name="catchAllExceptions">
		///		Specifies whether Start() will catch and gracefully report unhandled
		///		exceptions, which is typically what you want.  You can set this to 
		///		false while running under the debugger to allow it to break when an
		///		unhandled exception is thrown.
		/// </param>
		/// <returns>the application exit code to be returned by your "main" method</returns>

		protected int Start (string[] cmdLineParams, object paramsTarget, bool catchAllExceptions=true)
		{
			return this.Start(cmdLineParams, catchAllExceptions, paramsTarget);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// <para>Starts execution of the console application.  
		/// You should call this method in your "main" method as follows:</para>
		/// <code>return (new <i>MyConsoleAppSubclass</i>()).Start(<i>myCmdLineParams</i>, 
		///			!System.Diagnostics.Debugger.IsAttached, <i>myParamsObject</i>);</code>
		/// </summary>
		/// <param name="cmdLineParams">
		///		the command-line parameters array that was passed to your "main" method
		/// </param>
		/// <param name="catchAllExceptions">
		///		Specifies whether Start() will catch and gracefully report unhandled
		///		exceptions, which is typically what you want.  You can set this to 
		///		false while running under the debugger to allow it to break when an
		///		unhandled exception is thrown.
		/// </param>
		/// <param name="paramsTargets">
		///		Zero or more objects to receive command-line parameter values.
		///		If any are provided, then ParseCmdLineParams() will, using CmdLineParser, 
		///		parse the command line params into the paramsTarget.
		/// </param>
		/// <returns>the application exit code to be returned by your "main" method</returns>

		protected int Start (string[] cmdLineParams, bool catchAllExceptions, params object[] paramsTargets)
		{
			// NOTE: this method is the only place where SilentExitException should ever be caught

			this.StartThread = Thread.CurrentThread;

			if (!catchAllExceptions)
			{
				try { return this.UnsafeStart(cmdLineParams, paramsTargets); }
				catch (SilentExitException x)
				{
					if (x.ExitCode != null)
						return x.ExitCode.Value;
					return int.MaxValue;
				}
			}
			else
			{
				try
				{
					return this.UnsafeStart(cmdLineParams, paramsTargets);
				}
				catch (SilentExitException x)
				{
					if (x.ExitCode != null)
						return x.ExitCode.Value;
					return int.MaxValue;
				}
				catch (CmdLineSyntaxError x)
				{
					System.Console.Error.WriteLine("error in command-line parameters: " + x.Message);
					return ExitCodes.CmdLineParams.Value;
				}
				catch (Exception x)
				{
					System.Console.Error.Write("UNHANDLED EXCEPTION: ");
					ErrorUtils.DumpExceptionChain(x, System.Console.Error);
					return ExitCodes.CmdLineParams.Value;
				}
			}
		}

		//-----------------------------------------------------------------------------

		private int UnsafeStart (string[] cmdLineParams, params object[] paramsTargets)
		{
			this.CmdLineParamsTargets.AddRange(paramsTargets);
			this.PrintTitle(System.Console.Out);
			this.ParseCmdLineParams(cmdLineParams);
			return this.Run().Value;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Override this method to execute the main body of your application.
		/// Any exceptions thrown from within your Run method will be reported gracefully
		/// by the Start() method.
		/// </summary>

		protected abstract AppExitCode Run ( );

		//-----------------------------------------------------------------------------

		#endregion

		//-----------------------------------------------------------------------------
		/// <summary>
		/// If a paramsTarget object was passed to Start(), this method will use a
		/// CmdLineParser object to parse the command line params into the paramsTarget.
		/// Otherwise, if any command line params are present, this method will report
		/// a fatal error.  You can override this method to do your own command-line
		/// processing; any exceptions thrown will be reported gracefully.
		/// </summary>

		protected virtual void ParseCmdLineParams (string[] cmdLineParams)
		{
			if (this.CmdLineParamsTargets.Count == 0)
				throw new CmdLineSyntaxError("This program does not take any parameters.");
			else
			{
				CMD.CmdLineParser clp = new CMD.CmdLineParser();
				foreach (object target in this.CmdLineParamsTargets)
					clp.AddParams(target);
				clp.ParseCmdLine(cmdLineParams);
			}

			//if (this.CmdLineParamsTarget != null)
			//{
			//    CMD.CmdLineParser clp = new CMD.CmdLineParser(this.CmdLineParamsTarget);
			//    clp.ParseCmdLine(cmdLineParams);
			//}
			//else if (cmdLineParams.Length != 0)
			//	throw new CmdLineSyntaxError("This program does not take any parameters.");
		}

		//-----------------------------------------------------------------------------
		// Prints the name and version of the application.

		private void PrintTitle (TextWriter tw)
		{
			if (this.titlePrinted)
				return;
			this.titlePrinted = true;

#if SILVERLIGHT
			Assembly asm = Assembly.GetExecutingAssembly();
#else
			Assembly asm = Assembly.GetEntryAssembly();
#endif
			AssemblyName asmName = asm.GetName();

			string title = null;
			object[] attrs = asm.GetCustomAttributes(typeof(AssemblyTitleAttribute), false);
			if (attrs.Length > 0)
				title = ((AssemblyTitleAttribute) attrs[0]).Title;
			if (string.IsNullOrWhiteSpace(title))
				title = asmName.Name;

			string version = "ver. " + asmName.Version.ToString();

			// right-align the version number

			int lineWidth = 77;		// arbitrary default if not writing to console
#if !SILVERLIGHT
			if (tw == System.Console.Out || tw == System.Console.Error)
				lineWidth = System.Console.BufferWidth;
#endif

			int gap = lineWidth - (version.Length + title.Length);
			if (gap > 3)
			{
				gap = Math.Min(gap, 23);
				tw.WriteLine("\n" + title + "".PadRight(gap) + version + "\n");
			}
			else
			{
				tw.WriteLine("\n" + title);
				gap = Math.Min(0, lineWidth);
				tw.WriteLine("".PadRight(gap) + version + "\n");
			}
			
		}
		private bool titlePrinted = false;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Writes an error message to stderr (Console.Error) and exits the program.
		/// </summary>
		/// <param name="exitCode">the program exit code</param>
		/// <param name="lines">famous last words</param>

		public virtual void FatalError (AppExitCode exitCode, params string[] lines)
		{
			Require.NotNothing(exitCode);

			System.Console.Error.Write(this.FatalErrorPrefix);
			foreach (string line in lines)
				System.Console.Error.WriteLine(line);

			this.Exit(exitCode);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Exits the application by throwing a special exception that will be caught 
		/// (and thrown away) by Start().
		/// </summary>
		/// <param name="exitCode">the program exit code</param>

		public virtual void Exit (AppExitCode exitCode)
		{
			Require.Condition(Thread.CurrentThread == this.StartThread,
				"ConsoleApp does not currently support calling Exit() from a thread other than the one that invoked Start().");
			throw new SilentExitException(exitCode);
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// An exception that is intended to silently exit the application by
		/// returning from ConsoleApp.Start.  Don't catch it anywhere else!
		/// </summary>
		/// <remarks>
		/// The purpose of this is to enable you to set one breakpoint at the end of
		/// your Main method that will intercept (hopefully) all program terminations. 
		/// </remarks>

		private class SilentExitException : Exception 
		{
			public readonly AppExitCode ExitCode;
			public SilentExitException (AppExitCode x) { this.ExitCode = x; }
		}

		//-----------------------------------------------------------------------------
	}
}
